# 👉 Webpack 热替换
热替换原理(HotModuleReplacementPlugin,HMR)
热替换是指在无需完全刷新整个页面的情况下更新模块,只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。
# 原理
热替换的核心:
- 本地通过 express 搭建一个 dev-server 本地服务环境,启用 Websocket 和浏览器建立双向通信。当本地文件资源发生变化的时候,触发重新编译。
本地服务会在完成编译后,通过 sendStats 向浏览器推送更新,带上构建时的 hash 值,浏览器会将这个 hash 值与上一次资源进行差异对比。
(编译后的文件会打包到内存。这就是为什么在开发的过程中,你会发现 dist 目录没有打包后的代码,因为都在内存中。原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于 memory-fs。)
- 客户端在对比出差异后,会触发回调,这个回调会完成两件事情:
(1)进入 HotCheck,调用 hotDownloadManifest 发送 /hash.hot-update.json 请求,通过 json 请求结果获取热更新文件,以及下次热更新的 Hash 标识,并进入热更新准备阶段;
(2) HotCheck 确认需要热更新之后,进入 hotAddUpdateChunk 方法。
该方法先检查 Hash 标识的模块是否已更新,如果没更新,则通过在 DOM 中添加 Script 标签的方式,以 jsonp 的方式动态请求 js: /fileChunk.hash.hot-update.js,获取最新打包的 js 内容;
Jsonp 方式请求的原因:
主要是因为 JSONP 获取的代码可以直接执行。
为什么要直执行原因:
新编译后的代码是在一个 webpackHotUpdate 函数体内部的,所以要立即执行 webpackHotUpdate 这个方法。
HotModuleReplacementPlugin 会在 /fileChunk.hash.hot-update.js 中立即执行 webpackHotUpdate,通过直接遍历 moreModules,执行 hotApply 方法进行更新。
- 后续的部分,拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?
由 HotModuleReplacementPlugin 来完成。
# 具体步骤
启动本地服务前,调用了 updateCompiler(this.compiler)方法。这个方法中有 2 段关键性代码。
一个是获取 websocket 客户端代码路径,另一个是根据配置获取 webpack 热更新代码路径。
修改 entry 配置,新增上面的 websocket 客户端代码路径(webpack-dev-server/client/index.js)和获取热更新代码路径(webpack/hot/dev-server.js)
其中的(webpack-dev-server/client/index.js)是用于建立本地与浏览器 websocket 通信
修改好入口配置后,调用了 setupHooks 方法。这个方法是用来注册监听事件的,监听每次 webpack 编译完成。
监听到 webpack 编译完成后,会调用 _sendStats 方法通过 websocket 给浏览器发送通知,ok 和 hash 事件,这样浏览器就可以拿到最新的 hash 值了,然后浏览器做检查更新逻辑。
每次修改代码就会触发编译。说明我们还需要监听本地代码的变化,主要是通过 setupDevMiddleware 方法实现的。
webpack-dev-server 只负责启动服务和前置准备工作,而所有文件相关的操作都抽离到 webpack-dev-middleware 库了,主要是本地文件的编译和输出以及监听。
webpack-dev-middleware 做的事情:
(1)调用了 compiler.watch 方法监听本地文件的变化,主要是通过文件的生成时间是否有变化
(2)执行 setFs 方法,这个方法主要目的就是将编译后的文件打包到内存(而不用像发布生产环境那样子打包一个出 dist 文件夹),原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于 memory-fs。回到第四步,再完成编译以后会触发 _sendStats 方法通过 websocket 给浏览器发送通知,让浏览器判断是否需要热更新。
而此步骤相关联的就是第二步新增的入口文件路径:(webpack-dev-server/client/index.js)
这份代码里面会建立 websocket 和服务端的连接,并注册了 2 个监听事件。:
hash 事件,更新最新一次打包后的 hash 值。
ok 事件,进行热更新检查,热更新检查事件是调用 webpack 的 reloadApp 方法,利用 nodejs 的 EventEmitter 发出 webpackHotUpdate 消息。在第二步入口另一新增文件路径(webpack/hot/dev-server.js),里面的代码会监听 webpackHotUpdate 事件,一旦触发就回调获取最新的 hash 值,并且进行检查更新(终于检查更新了....,这一步是在浏览器中操作,代码是在运行中的环境)。
检查更新呢,调用的是 module.hot.check 方法。那么问题又来了,module.hot.check 又是哪里冒出来了的!答案是 HotModuleReplacementPlugin 搞得鬼。
HotModuleReplacementPlugin 会类似第二步一样默默的塞很多代码到 bundle.js。
HotModuleReplacementPlugin 会给 moudle 新增了一个属性为 hot,值为 hotCreateModule(moudleId)的方法。
而 module.hot.check 就是来自于 hotCreateModule(moudleId)方法。
module.hot.check 开启热替换准备
(1)利用上一次保存的 hash 值,调用 hotDownloadManifest 发送 xxx/hash.hot-update.json 的 ajax 请求;
调用 hotDownloadUpdateChunk 发送 xxx/hash.hot-update.js 请求,通过 JSONP 方式。
为什么使用 JSONP 获取最新代码?主要是因为 JSONP 获取的代码可以直接执行。
为什么要直接执行?因为 /hash.hot-update.js 里面新编译后的代码格式是在一个 webpackHotUpdate 函数体内部的。也就是要立即执行 webpackHotUpdate 这个方法。webpackHotUpdate 里面的代码:
window["webpackHotUpdate"] = function(chunkId, moreModules) { hotAddUpdateChunk(chunkId, moreModules); }; // hotAddUpdateChunk function hotAddUpdateChunk(chunkId, moreModules) { // 更新的模块moreModules赋值给全局全量hotUpdate for (var moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { hotUpdate[moduleId] = moreModules[moduleId]; } } // 调用hotApply进行模块的替换 hotUpdateDownloaded(); } // hotAddUpdateChunk方法会把更新的模块moreModules赋值给全局全量hotUpdate。 // hotUpdateDownloaded方法会调用hotApply进行代码的替换。
(2)请求结果获取热更新模块,以及下次热更新的 Hash 标识,并进入热更新准备阶段。
hotApply 热更新模块替换
热更新的核心逻辑就在 hotApply 方法了:
(1)删除过期的模块,就是需要替换的模块;
(2)将新的模块添加到 modules 中;
(3)通过 _webpack_require_ 执行相关模块的代码
此节主要参考文章:
轻松理解 webpack 热更新原理 (opens new window)
「吐血整理」再来一打 Webpack 面试题 (opens new window)
// TODO 待深入理解:
Webpack Hot Module Replacement 的原理解析 (opens new window)
← 👉 跨域 👉 Webpack 基础扫盲 →